/*
	Copyright (c) 2008 Memorphic Ltd. All rights reserved.
	
	Redistribution and use in source and binary forms, with or without 
	modification, are permitted provided that the following conditions 
	are met:
	
		* Redistributions of source code must retain the above copyright 
		notice, this list of conditions and the following disclaimer.
	    	
	    * Redistributions in binary form must reproduce the above 
	    copyright notice, this list of conditions and the following 
	    disclaimer in the documentation and/or other materials provided 
	    with the distribution.
	    	
	    * Neither the name of MEMORPHIC LTD nor the names of its 
	    contributors may be used to endorse or promote products derived 
	    from this software without specific prior written permission.
	
	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
	"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
	LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
	A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 
	OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
	SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
	LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
	DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 
	THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
	(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
	OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package memorphic.slide.metadata
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	
	import memorphic.slide.slide_private;
	
	import mx.binding.utils.ChangeWatcher;
	import mx.core.UIComponent;
	import mx.events.PropertyChangeEvent;
	import mx.utils.DescribeTypeCache;
	
	
	
	/**
	 * 
	 * This class adds the ability to use <code>[Cascade]</code> metadata annotations with any class that is in
	 * a hierarchical structure
	 */ 
	public class CascadeMetadataImpl extends EventDispatcher
	{
		
		private static const cascadeMap:Dictionary = new Dictionary(true);
		
		public var host:Object;
		
		public var isRoot:Boolean;
		
		public var parentDescr:ICascadeParentDescriptor;
		
		private var propertyCascadeMonitors:Object = new Object();
		
		
		private static const CONSTRUCTOR_KEY:Object = new Object();
		
		private var metadataHasBeenRead:Boolean = false;
		
		
		
		internal static function clearAllCascades():void
		{
			// TODO: This is mostly for use in unit tests tearDown()
			for each(var p:* in cascadeMap){
				delete cascadeMap[p];
			}
		}
		
		
		public static function create(host:Object, parentDescr:ICascadeParentDescriptor=null, isRoot:Boolean=false, readMetadata:Boolean=true):CascadeMetadataImpl
		{
			var impl:CascadeMetadataImpl = cascadeMap[host];
			if(parentDescr == null && !isRoot){
				if(!impl){
					parentDescr = new UIParentDescriptor(host as UIComponent);
				}	
			}
			if(impl){
				if(getQualifiedClassName(impl.parentDescr) != getQualifiedClassName(parentDescr)){
					throw new Error("Host '"+host+ "' already has a cascade implementation with an incompatible parent descriptor." );
				}
			}else{
				impl = new CascadeMetadataImpl(CONSTRUCTOR_KEY, host, parentDescr);
				impl.isRoot = isRoot;
				cascadeMap[host] = impl;
				if(readMetadata){
					impl.readMetadata();
				}
			}
			return impl;
		}
		
		
		public function CascadeMetadataImpl(key:Object, host:Object, parentDescr:ICascadeParentDescriptor)
		{
			if(key !== CONSTRUCTOR_KEY){
				throw new ArgumentError("CascadeMetadataImpl should be constructed with the static create() method.");
			}
			this.host = host;
			this.parentDescr = parentDescr;
			if(parentDescr){
				this.parentDescr.addEventListener(Event.CHANGE, onParentChange);
			}
			// TODO Add listener for parent change
		}
		
		private function onParentChange(e:Event):void
		{
			if(parentDescr.parentReady){
				for each(var c:CascadeMonitor in propertyCascadeMonitors){
					c.parentHandler();
				}
			}
		}
		
		
		slide_private function getParentPropertyValue(propName:String):*
		{
			use namespace slide_private;

			var parent:Object = parentDescr.getParent();
			if(parent == null){
				return null;
			}
			if(parent.hasOwnProperty(propName) && parent[propName] != null){
				return parent[propName];
			}else{ 
				var cascadeImpl:CascadeMetadataImpl = CascadeMetadataImpl.create(parent, parentDescr.copyDescriptor(parent));
				var monitor:CascadeMonitor = cascadeImpl.addCascade(propName, false, null, true);
				if(monitor.passive){
					monitor.addEventListener("propChange", CascadeMonitor(propertyCascadeMonitors[propName]).parentHandler);
				}
				return cascadeImpl.getParentPropertyValue(propName);
			}
		}
		
	
	
		/**
		 * 
		 * @param propName
		 * @param cloneProps
		 * @param cloneFunction This argument is not used at this time.
		 * @param passive
		 * @return 
		 * 
		 */	
		public function addCascade(propName:String, cloneProps:Boolean=false, cloneFunction:Function=null, passive:Boolean=false):CascadeMonitor
		{
			const parentView:Object = parentDescr.getParent();

			var eventName:String;
			var parentEventName:String;
			
			if(propertyCascadeMonitors[propName]){
				if(!passive && CascadeMonitor(propertyCascadeMonitors[propName]).passive){
					CascadeMonitor(propertyCascadeMonitors[propName]).passive = false;
				}
				return propertyCascadeMonitors[propName] as CascadeMonitor;
			}
			
			const cascadeMonitor:CascadeMonitor = new CascadeMonitor(host, this, propName, parentDescr, cloneProps, passive);
			propertyCascadeMonitors[propName] = cascadeMonitor;
			cascadeMonitor.explicitlySet = false;
			
			if(host[propName] == null && parentView){
				try {
					cascadeMonitor.updateProperty();
				}catch (e:ReferenceError){
					throw new Error("parent has no property '" + propName + "'");
				}
			}

			ChangeWatcher.watch(host, propName, cascadeMonitor.handler);
			if(!isRoot){
				ChangeWatcher.watch(parentView, propName, cascadeMonitor.parentHandler);
			}
			
			// make sure that the change handlers are triggered the first time
			// TODO: write a test that shows this is needed
			if(host[propName]){
				cascadeMonitor.init();
			}
			
			return cascadeMonitor;
		}
		
		
		public function get cascadePropNames():Array
		{
			var names:Array = new Array();
			for(var p:String in propertyCascadeMonitors){
				names.push(p);
			}
			return names;
		}
		
		
		
		/**
		 * Reads the [Cascade] metadata tags from the target object's type descriptor and adds the cascade behavior
		 * accordingly. Invoking this method more than once will not have any additional effect.
		 */		
		public function readMetadata():void
		{
			
			if(metadataHasBeenRead){
				return;
			}
			metadataHasBeenRead = true;
			
			//var parentView:Object = parentDescr.getParent();
			var desc:XML = DescribeTypeCache.describeType(host).typeDescription;
			var cascadeInfo:CascadeInfo = new CascadeInfo(desc);
			var cascaders:Object = cascadeInfo.getCascadingPropertyNames();
		
			for each(var item:CascadeInfoItem in cascaders){
				addCascade(item.propName, false, null, false);						
			}
			
		}
		
		
	}
}





	

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.EventPhase;
import flash.events.IEventDispatcher;

import mx.core.UIComponent;
import mx.events.PropertyChangeEvent;
import mx.utils.ObjectUtil;

import memorphic.slide.slide_private;
import memorphic.slide.metadata.ICascadeParentDescriptor;
import memorphic.slide.metadata.CascadeMetadataImpl;

	
class CascadeMonitor extends EventDispatcher
{
	
	private var host:Object;
	private var cascadeImpl:CascadeMetadataImpl;
	
	public var passive:Boolean;
	public var propName:String;
	public var explicitlySet:Boolean = false;
	private var selfUpdate:Boolean = false;
	
	public var useClones:Boolean;
	private var oldValue:Object;
	
	private var parentDescr:ICascadeParentDescriptor;
	
	public function CascadeMonitor(host:Object, cascadeImpl:CascadeMetadataImpl, propName:String, parentDescr:ICascadeParentDescriptor, useClones:Boolean, passive:Boolean)
	{
		this.host = host;
		this.cascadeImpl = cascadeImpl;
		this.propName = propName;
		this.parentDescr = parentDescr;
		this.useClones = useClones;
		this.passive = passive;
	}
	
	
	public function init():void
	{
		selfUpdate = true;
		try{
			hostValueChanged();
		}finally{
			selfUpdate = false;
		}
	}
	
	
	public function handler(e:Event=null):void
	{
		if(e != null && e is PropertyChangeEvent){
			if(PropertyChangeEvent(e).property != propName){
				return;
			}
		}
		hostValueChanged();
	}
	
	
	private function hostValueChanged():void
	{
		var newValue:Object = host[propName];
		if(!selfUpdate){
			selfUpdate = true;
			if(newValue == null){
				explicitlySet = false;
				if(parentDescr.getParent()){
					try{
						newValue = cascadeImpl.slide_private::getParentPropertyValue(propName);
					}catch(e:ReferenceError){
						newValue = null;
					}
				}
				updateProperty();
			}else{
				explicitlySet = true;
			}
			selfUpdate = false;
		}
		dispatchEvent(new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE, false, false, null, propName, oldValue, newValue));
		oldValue = newValue;	
	}
	
	
	public function parentHandler(e:Event=null):void
	{
		if(parentDescr.getParent()){
			if(!explicitlySet){
				selfUpdate = true;
				try{
					updateProperty();
				}finally{
					selfUpdate = false;
				}
			}			
		}else if(e){
			// XXX: I have a feeling that the one time we need to remove these is the
			// time when e == null.... look into this...
			IEventDispatcher(e.target).removeEventListener(e.type, parentHandler, true);
			IEventDispatcher(e.target).removeEventListener(e.type, parentHandler, false);
		}
	}
	
	public function updateProperty():void
	{
		use namespace slide_private;
		
		var newValue:Object = cascadeImpl.getParentPropertyValue(propName);
		
		if(passive){
			dispatchEvent(new Event("propChange"));
		}else{
			host[propName] = newValue;
		}
	}
	
}